Passed
Push — fix-null-terminated-string-spl... ( ec2073 )
by Jan
04:18
created

ID3Util.ts ➔ stringToEncodedBuffer   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 1
1
import iconv = require('iconv-lite')
2
import { FrameOptions, FRAME_OPTIONS } from './definitions/FrameOptions'
3
import { isKeyOf, isString } from './util'
4
5
export class SplitBuffer {
6
    value: Buffer | null
7
    remainder: Buffer | null
8
    constructor(value: Buffer | null = null, remainder: Buffer | null = null) {
9
        this.value = value
10
        this.remainder = remainder
11
    }
12
}
13
14
/**
15
 * Expects a buffer containing a string at the beginning that is terminated by a \0 character.
16
 * Returns a split buffer containing the bytes before and after null termination.
17
 */
18
export function splitNullTerminatedBuffer(buffer: Buffer, encodingByte = 0x00) {
19
    // UTF-16/BE always uses two bytes per character.
20
    // \0 is therefore encoded as [0x00, 0x00] instead of just [0x00].
21
    // We'll do a sliding window search, window size depends on encoding.
22
    const charSize = [0x01, 0x02].includes(encodingByte) ? 2 : 1
23
    for(let pos = 0; pos + charSize - 1 < buffer.length; pos += charSize) {
24
        if(buffer.readUIntBE(pos, charSize) === 0) {
25
            return new SplitBuffer(
26
                buffer.subarray(0, pos),
27
                buffer.subarray(pos + charSize)
28
            )
29
        }
30
    }
31
32
    return new SplitBuffer(null, buffer.subarray(0))
33
}
34
35
export function encodingFromStringOrByte(encoding: string | number) {
36
    const ENCODINGS = [
37
        'ISO-8859-1', 'UTF-16', 'UTF-16BE', 'UTF-8'
38
    ]
39
40
    if (isString(encoding) && ENCODINGS.includes(encoding)) {
41
        return encoding
42
    }
43
    if (
44
        typeof encoding === "number" &&
45
        encoding >= 0 && encoding < ENCODINGS.length
46
    ) {
47
        return ENCODINGS[encoding]
48
    }
49
    return ENCODINGS[0]
50
}
51
52
export function stringToEncodedBuffer(
53
    value: string,
54
    encodingByte: string | number
55
) {
56
    return iconv.encode(
57
        value,
58
        encodingFromStringOrByte(encodingByte)
59
    )
60
}
61
62
export function bufferToDecodedString(
63
    buffer: Buffer,
64
    encodingByte: string | number
65
) {
66
    return iconv.decode(
67
        buffer,
68
        encodingFromStringOrByte(encodingByte)
69
    ).replace(/\0/g, '')
70
}
71
72
export function getSpecOptions(frameIdentifier: string): FrameOptions {
73
    if (isKeyOf(frameIdentifier, FRAME_OPTIONS)) {
74
        return FRAME_OPTIONS[frameIdentifier]
75
    }
76
    return {
77
        multiple: false
78
    }
79
}
80
81
export function isValidID3Header(buffer: Buffer) {
82
    if (buffer.length < 10) {
83
        return false
84
    }
85
    if (buffer.readUIntBE(0, 3) !== 0x494433) {
86
        return false
87
    }
88
    if ([0x02, 0x03, 0x04].indexOf(buffer[3]) === -1 || buffer[4] !== 0x00) {
89
        return false
90
    }
91
    return isValidEncodedSize(buffer.subarray(6, 10))
92
}
93
94
/**
95
 * Returns -1 if no tag was found.
96
 */
97
export function getTagPosition(buffer: Buffer) {
98
    // Search Buffer for valid ID3 frame
99
    const tagHeaderSize = 10
100
    let position = -1
101
    let headerValid = false
102
    do {
103
        position = buffer.indexOf("ID3", position + 1)
104
        if (position !== -1) {
105
            // It's possible that there is a "ID3" sequence without being an
106
            // ID3 Frame, so we need to check for validity of the next 10 bytes.
107
            headerValid = isValidID3Header(
108
                buffer.subarray(position, position + tagHeaderSize)
109
            )
110
        }
111
    } while (position !== -1 && !headerValid)
112
113
    if (!headerValid) {
114
        return -1
115
    }
116
    return position
117
}
118
119
 export function isValidEncodedSize(encodedSize: Buffer) {
120
    // The size must not have the bit 7 set
121
    return ((
122
        encodedSize[0] |
123
        encodedSize[1] |
124
        encodedSize[2] |
125
        encodedSize[3]
126
    ) & 128) === 0
127
}
128
129
/**
130
 * ID3 header size uses only 7 bits of a byte, bit shift is needed.
131
 * @returns Return a Buffer of 4 bytes with the encoded size
132
 */
133
 export function encodeSize(size: number) {
134
    const byte_3 = size & 0x7F
135
    const byte_2 = (size >> 7) & 0x7F
136
    const byte_1 = (size >> 14) & 0x7F
137
    const byte_0 = (size >> 21) & 0x7F
138
    return Buffer.from([byte_0, byte_1, byte_2, byte_3])
139
}
140
141
/**
142
 * Decode the encoded size from an ID3 header.
143
 */
144
 export function decodeSize(encodedSize: Buffer) {
145
    return (
146
        (encodedSize[0] << 21) +
147
        (encodedSize[1] << 14) +
148
        (encodedSize[2] << 7) +
149
        encodedSize[3]
150
    )
151
}
152
153
export function getFrameSize(buffer: Buffer, decode: boolean, version: number) {
154
    const decodeBytes = version > 2 ?
155
        [buffer[4], buffer[5], buffer[6], buffer[7]] :
156
        [buffer[3], buffer[4], buffer[5]]
157
    if (decode) {
158
        return decodeSize(Buffer.from(decodeBytes))
159
    }
160
    return Buffer.from(decodeBytes).readUIntBE(0, decodeBytes.length)
161
}
162
163
export function parseTagHeaderFlags(header: Buffer) {
164
    if (!(header instanceof Buffer && header.length >= 10)) {
165
        return {}
166
    }
167
    const version = header[3]
168
    const flagsByte = header[5]
169
    if (version === 3) {
170
        return {
171
            unsynchronisation: !!(flagsByte & 128),
172
            extendedHeader: !!(flagsByte & 64),
173
            experimentalIndicator: !!(flagsByte & 32)
174
        }
175
    }
176
    if (version === 4) {
177
        return {
178
            unsynchronisation: !!(flagsByte & 128),
179
            extendedHeader: !!(flagsByte & 64),
180
            experimentalIndicator: !!(flagsByte & 32),
181
            footerPresent: !!(flagsByte & 16)
182
        }
183
    }
184
    return {}
185
}
186
187
export function processUnsynchronisedBuffer(buffer: Buffer) {
188
    const newDataArr = []
189
    if (buffer.length > 0) {
190
        newDataArr.push(buffer[0])
191
    }
192
    for(let i = 1; i < buffer.length; i++) {
193
        if (buffer[i - 1] === 0xFF && buffer[i] === 0x00) {
194
            continue
195
        }
196
        newDataArr.push(buffer[i])
197
    }
198
    return Buffer.from(newDataArr)
199
}
200
201
export function getPictureMimeTypeFromBuffer(pictureBuffer: Buffer) {
202
    if (
203
        pictureBuffer.length > 3 &&
204
        pictureBuffer.compare(Buffer.from([0xff, 0xd8, 0xff]), 0, 3, 0, 3) === 0
205
    ) {
206
        return "image/jpeg"
207
    }
208
    if (
209
        pictureBuffer.length > 8 &&
210
        pictureBuffer.compare(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), 0, 8, 0, 8) === 0
211
    ) {
212
        return "image/png"
213
    }
214
    return null
215
}
216